package cu.ds.logicalsync.fytasrealm.communication;

import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import net.jxta.endpoint.Message;
import net.jxta.endpoint.MessageElement;
import net.jxta.peer.PeerID;
import net.jxta.pipe.InputPipe;
import net.jxta.pipe.OutputPipe;
import net.jxta.pipe.OutputPipeEvent;
import net.jxta.pipe.OutputPipeListener;
import net.jxta.pipe.PipeMsgEvent;
import net.jxta.pipe.PipeMsgListener;
import net.jxta.pipe.PipeService;
import net.jxta.protocol.PipeAdvertisement;

import cu.ds.logicalsync.fytasrealm.communication.constants.Answer;
import cu.ds.logicalsync.fytasrealm.communication.constants.MapStatusRequest;
import cu.ds.logicalsync.fytasrealm.communication.event.MapStatusEvent;
import cu.ds.logicalsync.fytasrealm.communication.event.MapStatusListener;
import cu.ds.logicalsync.fytasrealm.communication.event.PeerStatusEvent;
import cu.ds.logicalsync.fytasrealm.communication.event.PeerStatusListener;
import cu.ds.logicalsync.fytasrealm.communication.model.PeerInfo;
import cu.ds.logicalsync.fytasrealm.model.Map;
import cu.ds.logicalsync.fytasrealm.model.MapState;
import cu.ds.logicalsync.fytasrealm.model.PositionMap;
import cu.ds.logicalsync.fytasrealm.model.World;
import cu.ds.logicalsync.fytasrealm.util.Tools;

/**
 * @author r.vashira
 *
 */
public class MapStatusService implements MapStatusServiceProvider {
	
	/** Member Variables **/
	private JxtaConnection connection;
	private MapStatusSender sender;
	private MapStatusReceiver receiver;
	private Map myMap;
	private World myWorld;
	private HashMap<MapStatusRequest, Answer> myAnswers;
	private Set<PeerInfo> peerCollection = new HashSet<PeerInfo>();
	
	
	/**
     * The table of peer status listeners.
     */
    private Set<MapStatusListener> listeners = new HashSet<MapStatusListener>();
    
    
    /** Properties **/
    public void setMyMap(Map myMap) {
		this.myMap = myMap;
	}
    
    public void setMyWorld(World myWorld) {
    	this.myWorld = myWorld;
    }
    
    public void setPeerCollection(Set<PeerInfo> peerCollection) {
    	this.peerCollection = peerCollection;
    }
    
    /**
	 * @param connection
	 */
	public MapStatusService(JxtaConnection connection) {
		super();
		this.connection = connection;
		
		// initial member variables
		myAnswers = new HashMap<MapStatusRequest, Answer>();
		
		//TODO: Create MessageSender and Receiver for MapStatusService
		sender = new MapStatusSender(this.connection);
		receiver = new MapStatusReceiver(this.connection);
	}

	
	/**
	 * {@inheritDoc} 
	 * @throws CommunicationException 
	 */
	public World createNewWorld() throws CommunicationException {
		// create new world by radom position i,j on map then ask peer to consensus answers
		World result = null;
		boolean isCreateSuccess = false;
		
		while(!isCreateSuccess) {
			result = tryCreateNewWorld();
			
			PositionMap positionOnMap = result.getMyPositionInMap();
			int col = positionOnMap.getColPosition();
			int row = positionOnMap.getRowPosition();
			System.out.println("Create new World at position : [" + row + "]" + "[" + col + "]");
			
			// FIXME: talk with Eii that Map.setMapRoom(i,j,action) should create new instance of PositionMap
			// How to tell the Map that position is preserved?
			if((myMap != null) && (MapState.Empty.equals(myMap.getState()))) {
				isCreateSuccess = true;
			}
			
			// TODO: registerMap <-- inputPipe, then update isCreateSuccess in outputPipe
			// FIXME: check with testing
			
			// actual working on p2p!
			// use sender to broadcast request
			sender.requestRegisterWorldInMap(this.connection.getMyPeerID(), result);
			// TODO: remove this line when call actual sender
			isCreateSuccess = true;
		}
		
		
		return result;
	}
	
	/**
	 * Request
	 * @return
	 */
	private World tryCreateNewWorld() {
		World candidate = new World(connection.getMyPeerName());
		return candidate;
	}
	
	
	private void notifyListeners() {
		//TODO: collect all participant from outputPipe
		Set<PeerInfo> participants = new HashSet<PeerInfo>();
		
		MapStatusEvent event = new MapStatusEvent(this, participants, myMap);
		MapStatusListener[] allListeners = listeners.toArray(new MapStatusListener[0]);
		for (MapStatusListener listener : allListeners) {
			listener.mapUpdated(event);
		}
	}
	
	/**
	 * {@inheritDoc} 
	 */
	public synchronized void addListener(MapStatusListener listener) {
		listeners.add(listener);
	}

	/**
	 * {@inheritDoc} 
	 */
	public boolean removeListener(MapStatusListener listener) {
		return listeners.remove(listener);
	}

	/**
	 * {@inheritDoc} 
	 */
	public void start() {
		// TODO: run background service here, if any
		try {
			receiver.listenBroadcastRequests();
		} catch (CommunicationException e) {
			e.printStackTrace();
			Tools.popErrorMsg("MapStatusService.start()", e.getMessage());
		}
	}

	/**
	 * {@inheritDoc} 
	 */
	public void stop() {
		// Should we release resources holding?
	}
	
	
	/**** Event Helper Methods ****/
	
	/**
	 * Process request from MapStatusReceiver
	 * 
	 * @param request
	 */
	private void processRequest(MapStatusRequest request, Message msg) {
		switch(request) {
		case RegisterWorldInMap:
			try {
				sender.replyRegisterWorldInMap(msg);
			} catch (CommunicationException e) {
				e.printStackTrace();
			}
			break;
		case ReplyRegisterWorldInMap: 
		{
			
			
			// [event] mapUpdated
			notifyListeners();
			break;
		}
		case NotSpecified:
			break;
		}
	}
	
	
	/**** Anonymous Class ****/
	
	
	/**
	 * Map Status Sender which send update status to MapStatusReceiver
	 * 
	 * @author r.vashira
	 * @see MapStatusReceiver
	 */
	protected class MapStatusSender implements OutputPipeListener {
		private JxtaConnection connection;
		// the pipe service for p2p asynchronous binding
		private PipeService pipeService;
		private OutputPipe outputPipe;
		private Message message;
		// for broadcast communication
		/**
		 * @deprecated
		 */
		private PipeAdvertisement globalMapConnectionAddress;
		// for 1-1 communication
		private PipeAdvertisement directConnectionAddress;
		
		// for message passing
		//private World worldToRegister;
		
		public MapStatusSender(JxtaConnection connection) {
			super();
			this.connection = connection;
			
			// initial pipe
			pipeService = this.connection.getPipeService();
			globalMapConnectionAddress = this.connection.getMapPipeAdvertisement();
		}

		public void requestRegisterWorldInMap(PeerID requester, World worldToRegister) throws CommunicationException {
			PositionMap requestPosition = worldToRegister.getMyPositionInMap();
			
			// prepare request
			message = JxtaUtil.createRequestMessage(MapStatusRequest.RegisterWorldInMap, "MapStatusService");
			JxtaUtil.addStringToMessage(message, "MapStatusService", "RequestPeerID", requester.toString());
			JxtaUtil.addStringToMessage(message, "MapStatusService", "Row", Integer.toString(requestPosition.getRowPosition()));
			JxtaUtil.addStringToMessage(message, "MapStatusService", "Col", Integer.toString(requestPosition.getColPosition()));
			
			PeerInfo[] allPeers = peerCollection.toArray(new PeerInfo[0]);
			for (PeerInfo currentPeer : allPeers) {
				
				// issue a pipe resolution asynchronously. outputPipeEvent() is called
		        // once the pipe has resolved
		        try {
		        	PipeAdvertisement peerAddress = currentPeer.getAddress();
					pipeService.createOutputPipe(peerAddress, this);
				} catch (IOException e) {
					//System.out.println("OutputPipe creation failure");
					//e.printStackTrace();
					throw new CommunicationException("MapStatusSender - OutputPipe creation failure", e);
				}

			}
			
		}
		
		/**
		 * @deprecated
		 * @param requester
		 * @param worldToRegister
		 * @throws CommunicationException
		 */
		public void requestRegisterWorldInMap2(PeerID requester, World worldToRegister) throws CommunicationException {
			// persist to memory
			//this.worldToRegister = worldToRegister;
			PositionMap requestPosition = worldToRegister.getMyPositionInMap();
			//requestPosition.
			
			// prepare request
			message = JxtaUtil.createRequestMessage(MapStatusRequest.RegisterWorldInMap, "MapStatusService");
			JxtaUtil.addStringToMessage(message, "MapStatusService", "RequestPeerID", requester.toString());
			JxtaUtil.addStringToMessage(message, "MapStatusService", "Row", Integer.toString(requestPosition.getRowPosition()));
			JxtaUtil.addStringToMessage(message, "MapStatusService", "Col", Integer.toString(requestPosition.getColPosition()));
			
			// issue a pipe resolution asynchronously. outputPipeEvent() is called
	        // once the pipe has resolved
	        try {
				pipeService.createOutputPipe(globalMapConnectionAddress, this);
			} catch (IOException e) {
				//System.out.println("OutputPipe creation failure");
				//e.printStackTrace();
				throw new CommunicationException("MapStatusSender - OutputPipe creation failure", e);
			}
		}
		
		/**
		 * Reply
		 * @throws CommunicationException 
		 */
		private void replyRegisterWorldInMap(Message msg) throws CommunicationException {
			String requesterPeerID = JxtaUtil.getStringFromMessage(msg, "MapStatusService", "RequestPeerID");
			String rowPositionStr = JxtaUtil.getStringFromMessage(msg, "MapStatusService", "Row");
			String colPositionStr = JxtaUtil.getStringFromMessage(msg, "MapStatusService", "Col");
			int rowPositionInMap = Integer.parseInt(rowPositionStr);
			int colPositionInMap = Integer.parseInt(colPositionStr);
			Answer answer = Answer.Yes;
			
			// if the request position is collide with our World, reject
			if(myWorld != null) {
				// if we are the first person in game, then World is not created yet
				PositionMap myPosition = myWorld.getMyPositionInMap();
				// Collide
				if((myPosition.getRowPosition() == rowPositionInMap) && 
						(myPosition.getColPosition() == colPositionInMap)) {
					answer = Answer.No;
				}
			}
			
			// prepare reply
			message = JxtaUtil.createRequestMessage(MapStatusRequest.ReplyRegisterWorldInMap, "MapStatusService");
			JxtaUtil.addStringToMessage(message, "MapStatusService", "ReplyPeerID", connection.getMyPeerID().toString());
			JxtaUtil.addStringToMessage(message, "MapStatusService", "Answer", answer.toString());
			
			// issue a pipe resolution asynchronously. outputPipeEvent() is called
	        // once the pipe has resolved
	        try {
				pipeService.createOutputPipe(globalMapConnectionAddress, this);
			} catch (IOException e) {
				System.out.println("OutputPipe creation failure");
				e.printStackTrace();
				throw new CommunicationException("MapStatusSender - OutputPipe creation failure", e);
			}
		}
		
		/**
		 * {@inheritDoc} 
		 */
		public void outputPipeEvent(OutputPipeEvent event) {
			System.out.println("MapStatusSender - Received the output pipe resolution event");
			outputPipe = event.getOutputPipe();
			
			try {
				System.out.println("MapStatusSender - Sending message");
				// send the message
				// FIXME: polymorph here to allow multiple message type
				outputPipe.send(this.message);
				System.out.println("MapStatusSender - Message sent");
			} catch (IOException e) {
				System.out.println("Failed to send message");
				e.printStackTrace();
				//System.exit(-1);
				Tools.popErrorMsg("MaspStatusSender", "Failed to send message");
			}
		}
		
	}
	
	/**
	 * Map Status Receiver which receive request and update Answers from MapStatusSender
	 * 
	 * @author r.vashira
	 * @see MapStatusSender
	 */
	protected class MapStatusReceiver implements PipeMsgListener {
		private JxtaConnection connection;
		//FIXME: release inputPipe after use, inputPipe.close();
		private InputPipe inputPipe;
		
		/**
		 * @param connection
		 */
		public MapStatusReceiver(JxtaConnection connection) {
			super();
			this.connection = connection;	
		}
		
		public void listenBroadcastRequests() throws CommunicationException {
			try {
	            System.out.println("Creating input pipe");
	            // Create the InputPipe and register this for message arrival
	            // notification call-back
	            PipeService pipeService = connection.getPipeService();
	            PipeAdvertisement globalMapConnectionAddress = connection.getMapPipeAdvertisement();
	            
	            inputPipe = pipeService.createInputPipe(globalMapConnectionAddress, this);
	        } catch (IOException io) {
	            //io.printStackTrace();
	           // return;
	            throw new CommunicationException("MapStatusReceiver - listenBroadcastRequests failure", io);
	        }
	        if (inputPipe == null) {
	            //System.out.println(" cannot open InputPipe");
	            //System.exit(-1);
	        	throw new CommunicationException("MapStatusReceiver - cannot open InputPipe");
	        }
	        System.out.println("MapStatusService - Waiting for msgs on input pipe");
		}

		/**
		 * {@inheritDoc} 
		 */
		public void pipeMsgEvent(PipeMsgEvent event) {
			Message msg;
	        try {
	            // Obtain the message from the event
	            msg = event.getMessage();
	            if (msg == null) {
	                System.out.println("Received an empty message");
	                return;
	            }
	            // dump the message content to screen
	            JxtaUtil.printMessageStats(msg, true);
	        } catch (Exception e) {
	            e.printStackTrace();
	            return;
	        }

	        // get all the message elements
	        Message.ElementIterator en = msg.getMessageElements();

	        if (!en.hasNext()) {
	            return;
	        }

	        // FIXME: polymorph here to allow multiple message type
	        // get the message element in the name space PipeClient.MESSAGE_NAME_SPACE
	        MessageElement msgElement = msg.getMessageElement(null, MapStatusRequest.RegisterWorldInMap.toString());
	        String requestTypeStr = JxtaUtil.getStringFromMessage(msg, "MapStatusService", JxtaUtil.REQUEST_TYPE);
	        String requesterPeerID = JxtaUtil.getStringFromMessage(msg, "MapStatusService", "RequestPeerID");
	        
	        // parse enum
	        MapStatusRequest requestType = MapStatusRequest.valueOf(requestTypeStr);
	        System.out.println("RequestType: " + requestType.toString() + " received from " + requesterPeerID);
	        
	        String myPeerID = connection.getMyPeerID().toString();
	        if(requesterPeerID.equals(myPeerID))
	        {
	        	System.out.println("My own request! - skip");
	        	return;
	        }

	        // Get message
	        if (msgElement.toString() == null) {
	            System.out.println("null msg received");
	        } else {
	            Date date = new Date(System.currentTimeMillis());
	            System.out.println("Message received at :" + date.toString());
	            System.out.println("Message created at:" + msgElement.toString());
	            
	            //TODO: answers request based on information we have.
	            // Call MapStatusService to process request
		        processRequest(requestType, msg);
	        }
		}
		
	}

}
